Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial changes for saving product images upload statuses in UserDefaults #15196

Open
wants to merge 29 commits into
base: trunk
Choose a base branch
from

Conversation

pmusolino
Copy link
Member

@pmusolino pmusolino commented Feb 18, 2025

Description

Part 1 of the changes needed for product image uploads in the background.

  • Purpose: Implement initial changes to save product image upload statuses in UserDefaults. (The actual save will happen in another PR) Ref. pe5sF9-3Uc
  • Why:
    • Required for background image uploads.
    • Ensure a stored queue of image statuses persists when the app is in the background.
    • Queue includes siteID and productID.
  • Background:
    • URLSession background configuration doesn't have access to ServiceLocator or similar classes.
    • System processes every element in scheduled URLSession upload tasks.
    • A queue must function even when the app isn't running.
  • Implementation:
    • Introduces ProductImagesUserDefaultsStatuses for storing statuses in UserDefaults.
    • Not currently used; will be implemented in a future PR.
    • Suggest skipping the review of ProductImagesUserDefaultsStatuses due to significant changes in a subsequent PR.
  • Additional Changes:
    • More changes than expected due to file relocations.

Questions that may arise:

  • Why use siteID and productID in ProductImageStatus?:
    • Background URLSession doesn't allow request updates during execution or scheduling. If the app goes in background, we do not have any power on it.
    • After user actions like pressing save, updating requests isn't feasible.
    • Then, Storing IDs helps check statuses from UserDefaults and send subsequent requests at the end of every future upload request.
    • Facilitates progress recovery and error management upon app re-launch from the background.

Main changes applied in this PR:

  • File Movements:
    • Moved ProductImageStatus to the Networking layer for background URLSession access.
    • Moved ProductOrVariationID enum to Networking layer for consistency.
  • Enhancements:
    • ProductImageStatus and ProductImageAssetType now conform to Codable for UserDefaults storage.
    • ProductImageStatus enum cases (uploading, remote, uploadFailure) now accept siteID and productID.
  • New Implementation:
    • ProductImagesUserDefaultsStatuses for ProductImageStatus storage.
    • Future changes expected, with unit tests to be added.
  • Code Updates:
    • Updated image upload classes to handle siteID and productID.
    • Updated related unit tests.

Testing information

  • Scenarios Tested:
    • Uploading multiple images in a new product, both before and after publishing.
    • Uploading multiple images in an existing product, both before and after saving.
    • Selecting and saving new images from the WordPress media library.
    • Ensuring image re-ordering functionality.
    • Uploading images to product variations and saving them before/after upload completion.
    • Managing image upload errors effectively.

  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

Reviewer (or Author, in the case of optional code reviews):

Please make sure these conditions are met before approving the PR, or request changes if the PR needs improvement:

  • The PR is small and has a clear, single focus, or a valid explanation is provided in the description. If needed, please request to split it into smaller PRs.
  • Ensure Adequate Unit Test Coverage: The changes are reasonably covered by unit tests or an explanation is provided in the PR description.
  • Manual Testing: The author listed all the tests they ran, including smoke tests when needed (e.g., for refactorings). The reviewer confirmed that the PR works as expected on all devices (phone/tablet) and no regressions are added.

Summary by CodeRabbit

  • New Features
    • Enhanced image upload handling to provide more robust status tracking and improved reliability.
  • Refactor
    • Consolidated image management logic to ensure consistent state updates and streamlined user interactions.
  • Tests
    • Expanded testing of image upload flows and status updates, ensuring a smoother experience when managing product images.

…es in UserDefaults

- Modify `ProductImageStatus.swift` to add Codable conformance and additional properties
- Create `ProductImagesUserDefaultsStatuses.swift` for managing product image statuses in User Defaults
- Move `ProductOrVariationID` in a separate file to define a type-safe identifier for products and variations
- Update `Model.swift` to include `ProductOrVariationID` typealias
- Update `ProductImagesCollectionViewDataSource.swift` to include siteID and productID in `ProductImageStatus` cases.
- Modify `ProductImagesHeaderTableViewCell.swift` to handle additional parameters in `ProductImageStatus`.
- Adjust `Product+Media.swift` to pass siteID and productID when mapping images to `ProductImageStatus`.
- Change `ProductImageActionHandler.swift` to incorporate siteID and productID in `ProductImageStatus` handling and updating.
- Update `ProductImageStatus+Extension.swift` to manage new parameters in `ProductImageStatus`.
- Amend `ProductImagesCollectionViewController.swift` to handle siteID and productID in status cases.
- Modify `ProductImagesSaver.swift` to use siteID and productID in status comparison and updates.
- Adjust `ProductImagesViewController.swift` to manage `ProductImageStatus` with siteID and productID.
… all statuses without the correct value are now updated. This ensures that even if the statuses were initially created with a nil or outdated productID, they will be updated to the new remoteProductID.
…uploading` and `uploadFailure` cases

- Modify `ProductImagesUserDefaultsStatuses.swift` to handle optional `productID` in filtering statuses
@pmusolino pmusolino added this to the 21.8 milestone Feb 18, 2025
@pmusolino pmusolino added feature: product details Related to adding or editing products, including Product Settings. feature: add/edit products Related to adding or editing products. labels Feb 18, 2025
@dangermattic
Copy link
Collaborator

dangermattic commented Feb 18, 2025

1 Warning
⚠️ This PR is larger than 300 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.

Generated by 🚫 Danger

@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Feb 18, 2025

WooCommerce iOS📲 You can test the changes from this Pull Request in WooCommerce iOS by scanning the QR code below to install the corresponding build.

App NameWooCommerce iOS WooCommerce iOS
Build Numberpr15196-6ea99b3
Version21.8
Bundle IDcom.automattic.alpha.woocommerce
Commit6ea99b3
App Center BuildWooCommerce - Prototype Builds #13163
Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.

@pmusolino pmusolino changed the title Feat/save product image upload statuses in user defaults Initial changes for saving product images upload statuses in UserDefaults Feb 18, 2025
@wpmobilebot wpmobilebot modified the milestones: 21.8, 21.9 Feb 21, 2025
@wpmobilebot
Copy link
Collaborator

Version 21.8 has now entered code-freeze, so the milestone of this PR has been updated to 21.9.

…e-product-image-upload-statuses-in-user-defaults
…iationID` in `uploading` and `uploadFailure` cases

- Update unit tests
…pending upload, then trigger the media upload after adding the observer.
…ic. Instead of direct PHAsset comparison, compare localIdentifier to ensure accurate detection and state updates from "uploading" to "remote." This resolves the issue where hasUnsavedChangesOnImages(...) incorrectly returns true after saving, fixing related test failures.
@pmusolino pmusolino marked this pull request as ready for review February 24, 2025 17:11
@itsmeichigo itsmeichigo self-assigned this Feb 25, 2025
Copy link
Contributor

@itsmeichigo itsmeichigo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Paolo, this PR is really large so it was hard to review. I'm leaving all my comments here first before testing the app.

Next time, it'd be great to split PRs into smaller pieces and have thorough tests for each change rather than wrapping everything in one go. For example:

  • Part 1: Create copies of ProductImageStatus, ProductOrVariationID in the Networking layer and add tests for the codable conformance.
  • Part 2: Add the storage for product image status and unit test it.
  • Part 3: Replace the networking types in the UI layer.

Comment on lines +180 to +186
// If the product is a variation, substitute the existing status with the new uploading status.
// Otherwise, if a standard product, append a new status.
if case .variation = self.productOrVariationID {
self.productImageStatuses = [uploadingStatus]
} else {
self.productImageStatuses = [uploadingStatus] + self.productImageStatuses
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this check is correct as we allow only one image for variations, the logic should not be part of ProductImageActionhandler IMO. The UI should already take care of disabling uploading more than one image for variations. I'd suggest restoring the previous logic.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a safeguard that makes sense to keep. The ProductImageActionHandler is one of the central pieces that manages and tracks image upload statuses. Even if the UI intends to allow only one image for variations, the handler is the final arbiter. And in fact, reverting the code generate a crash because the UI is not managing in a proper way the image statuses, replaing the current images showed. This means that if you upload an image in a variation, save it, then you try to replace it, you can expect a crash because in the end the images will be two (the remote one + the new one).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you suggesting that the previous logic is causing a crash on trunk? I tried as you suggested but couldn't reproduce the crash.

I think the crash should not happen because when you replace an image, the remote image is discarded from the status list.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tagging @pmusolino in case this message got lost is the depth of discussions here. Could you clarify my last question above please?

Copy link
Member Author

@pmusolino pmusolino Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@itsmeichigo No, I meant that given how the status search now works with siteID and productID, this looks like a necessary change in this PR. In trunk, everything should work as before.

/// Save product image upload statuses in User Defaults.
/// This class is declared in the Networking layer because it will also be accessed by the background URLSession operations.
///
final class ProductImagesUserDefaultsStatuses {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider renaming this to ProductImageStatusStorage and injecting user defaults into its initializer for testability.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file has been extensively revised in a future PR, where UserDefaults is also injected for testability. Anyway, the suggested name seems interesting to me. Since this file has been thoroughly revisited in future PRs, I will rename it there. 👍

case "uploadFailure":
let asset = try container.decode(ProductImageAssetType.self, forKey: .asset)
let errorMessage = try container.decode(String.self, forKey: .error)
let error = NSError(domain: "ProductImageStatus", code: 0, userInfo: [NSLocalizedDescriptionKey: errorMessage])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we'll lose information about the error here. The information is necessary because we're showing details about the upload failures. Please also retain the domain and error code while encoding/decoding.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been handled in a separate PR, but I'm applying the changes from that PR in this one. 👍

try container.encode("uploading", forKey: .type)
try container.encode(asset, forKey: .asset)
try container.encode(siteID, forKey: .siteID)
try container.encodeIfPresent(productID, forKey: .productID)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why encodeIfPresent is necessary? I think productID is non-optional here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. In fact, in a following PR, this has been changed. Like above, I'm applying the changes from that PR in this one.

if let filterProductID = productID {
return sID == siteID && pID == filterProductID
} else {
return sID == siteID && pID == nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Product ID is non-optional in image statuses so this check will likely fail.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the PR description, I suggested not reviewing this file since it is not used and, above all, it has been deeply modified in a future PR (specifically in this one, which will be split into multiple PRs due to its size). This code no longer exists there 👍 so you can ignore it for the moment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this file is not ready, please do not commit it into a PR like this. It would be hard for me to approve the PR because not everything is working and I cannot guarantee the quality of the code to be merged.

- Add new cases for `errorDomain`, `errorCode`, and `errorUserInfo` in `ProductImageStatus.swift`.
- Modify decoding logic to handle detailed error information.
- Update encoding logic to include `errorDomain`, `errorCode`, and `errorUserInfo`.
- Change `productID` to be always encoded in `uploading` and `uploadFailure` cases.
Copy link

coderabbitai bot commented Feb 25, 2025

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

The pull request introduces new Swift files and updates to existing ones to enhance product image status management. Three new files—defining enums and a status management class—are organized in a new group within the Xcode project. Changes extend the image status types by adding parameters (siteID and productID) and refactor existing methods, switch cases, and tests accordingly. The pull request also removes redundant enum definitions and updates project file references and public type aliases to align with the new structure.

Changes

File(s) Summary of Changes
Networking/Networking.xcodeproj/project.pbxproj
Networking/Networking/ProductImageInBackground/ProductImageStatus.swift
Networking/Networking/ProductImageInBackground/ProductImagesUserDefaultsStatuses.swift
Networking/Networking/ProductImageInBackground/ProductOrVariationID.swift
Added three new Swift files with public enums and a status management class; created a new project group ProductImageInBackground; updated project configuration and file references.
RELEASE-NOTES.txt Added an internal note for version 21.9 indicating that siteID and productID are now assigned to image product upload statuses.
WooCommerce/Classes/ServiceLocator/ProductImageUploader.swift
WooCommerce/Classes/ViewRelated/Products/Cells/Product Images/ProductImagesCollectionViewDataSource.swift
WooCommerce/Classes/ViewRelated/Products/Cells/Product Images/ProductImagesHeaderTableViewCell.swift
WooCommerce/Classes/ViewRelated/Products/Media/Product+Media.swift
WooCommerce/Classes/ViewRelated/Products/Media/ProductImageActionHandler.swift
WooCommerce/Classes/ViewRelated/Products/Media/ProductImageStatus+Extension.swift
WooCommerce/Classes/ViewRelated/Products/Media/ProductImagesCollectionViewController.swift
WooCommerce/Classes/ViewRelated/Products/Media/ProductImagesSaver.swift
WooCommerce/Classes/ViewRelated/Products/Media/ProductImagesViewController.swift
Refactored image status handling to include additional parameters (siteID, productID); removed redundant enum definitions; updated switch cases, method signatures, and computed properties to accommodate new data structures.
WooCommerce/WooCommerce.xcodeproj/project.pbxproj Updated file references – renamed ProductImageStatus.swift to ProductImageStatus+Extension.swift and adjusted references for MockProductImageActionHandler.swift.
WooCommerce/WooCommerceTests/Mocks/MockProductImageActionHandler.swift
WooCommerce/WooCommerceTests/ViewRelated/Products/Edit Product/ProductVariationFormViewModel+ImageUploaderTests.swift
WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageActionHandlerTests.swift
WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageStatus+HelpersTests.swift
WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageUploaderTests.swift
WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImagesSaverTests.swift
Updated tests to include new siteID and productID properties; modified status constructions and assertions to reflect the enhanced image status structure.
Yosemite/Yosemite/Model/Model.swift Added new public type aliases for ProductImageStatus, ProductImageAssetType, and ProductOrVariationID, mapping them to the corresponding types in the Networking module.

Sequence Diagram(s)

sequenceDiagram
    participant UI as ViewController
    participant Uploader as ProductImageActionHandler
    participant Storage as ProductImagesUserDefaultsStatuses
    participant Backend as Server

    UI->>Uploader: Initiate image upload (asset, siteID, productID)
    Uploader->>Storage: addStatus(.uploading(asset, siteID, productID))
    Uploader->>Backend: Upload asset
    Backend-->>Uploader: Upload response (success/failure)
    alt Success
        Uploader->>Storage: updateStatus(.remote(image, siteID, productID))
    else Failure
        Uploader->>Storage: updateStatus(.uploadFailure(asset, error, siteID, productID))
    end
    Uploader-->>UI: Notify upload result
Loading

Poem

In my burrow of code, I hop with delight,
Adding enums and classes, making statuses bright.
Files multiply like carrots in a row,
With siteID and productID giving them flow.
I nibble on tests and watch features bloom—
A happy rabbit sings in ASCII gloom.
🐇💻✨


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@pmusolino pmusolino changed the title Initial changes for saving product images upload statuses in UserDefaults Initial changes for saving product images upload statuses in UserDefaults - Part 1 Feb 25, 2025
@pmusolino pmusolino changed the title Initial changes for saving product images upload statuses in UserDefaults - Part 1 Initial changes for saving product images upload statuses in UserDefaults Feb 25, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🔭 Outside diff range comments (1)
WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageUploaderTests.swift (1)

1-718: 🛠️ Refactor suggestion

Missing unit tests for the new UserDefaults functionality.

According to the PR objectives, this change introduces the foundation for storing product image upload statuses in UserDefaults. However, there are no tests directly verifying this functionality. Consider adding specific tests that check if statuses are properly saved to and retrieved from UserDefaults.

Would you like me to generate a sample test case for verifying the UserDefaults functionality?

🧹 Nitpick comments (13)
WooCommerce/Classes/ViewRelated/Products/Cells/Product Images/ProductImagesHeaderTableViewCell.swift (1)

79-79: Consider utilizing the additional parameters in error handling

Similar to the .remote case, the .uploadFailure case now includes siteID and productID parameters that are being ignored. Since these provide context about where the failure occurred, consider whether these should be passed to the onFailedUploadSelected handler to improve error reporting or recovery.

-case let .uploadFailure(asset, error, _, _):
-    onFailedUploadSelected?(asset, error)
+case let .uploadFailure(asset, error, siteID, productID):
+    onFailedUploadSelected?(asset, error, siteID, productID)

Note: This would require updating the onFailedUploadSelected closure definition and all implementations.

Networking/Networking/ProductImageInBackground/ProductOrVariationID.swift (2)

7-14: Consider adding a productID accessor method

The id property returns different values based on the enum case - product ID for .product and variation ID for .variation. For scenarios where you specifically need the product ID regardless of case, consider adding a dedicated accessor.

+/// Returns the product ID for both product and variation types
+public var productID: Int64 {
+    switch self {
+    case .product(let id):
+        return id
+    case .variation(let productID, _):
+        return productID
+    }
+}

28-40: Add error handling for incomplete JSON

The decoder implementation should handle cases where the required fields are missing or have incorrect types. Consider adding more robust error handling or using default values where appropriate.

public init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
-   let caseType = try container.decode(CaseType.self, forKey: .type)
+   // Handle missing or invalid type with a custom error message
+   guard let caseType = try? container.decode(CaseType.self, forKey: .type) else {
+       throw DecodingError.dataCorruptedError(
+           forKey: .type,
+           in: container,
+           debugDescription: "Invalid or missing ProductOrVariationID type"
+       )
+   }
    
    switch caseType {
    // ... rest of the implementation remains the same
Networking/Networking/ProductImageInBackground/ProductImageStatus.swift (3)

19-19: Ensure robust error encoding/decoding.

Currently, the code assumes the NSError.userInfo is [String: String]?. This might cause data loss if the error contains more complex information. Consider either serializing the entire userInfo more flexibly or storing a simplified representation of the error.


127-135: Check concurrency and availability of PHAsset fetch.

Decoding a phAsset by synchronously calling PHAsset.fetchAssets can block if performed on the main thread. Consider offloading this fetch or marking the decoding process to run on a background thread to improve responsiveness.


160-166: Evaluate PNG-only serialization for UIImages.

Relying on pngData() might lead to large serialized data. Using JPEG compression or other strategies may reduce payload size while maintaining acceptable quality.

WooCommerce/Classes/ViewRelated/Products/Media/ProductImagesCollectionViewController.swift (2)

90-92: Updated pattern matching with unused parameters.

The pattern matching has been correctly updated to match the new ProductImageStatus enum cases while preserving the existing functionality.

Consider making the parameter names explicit for better code readability, even though they're not used in this context:

-case .remote(let image, _, _):
+case .remote(let image, let siteID, let productID):

This would make it clearer to future developers what these parameters represent, though it's perfectly fine to use underscores for unused parameters.


90-106: Consider addressing unused parameters.

All switch cases use underscores for unused parameters. While this is standard Swift practice, a previous reviewer suggested omitting unused params.

You could refactor the ProductImageStatus enum to separate the status from its metadata, or create helper methods that extract only needed information.

Networking/Networking/ProductImageInBackground/ProductImagesUserDefaultsStatuses.swift (3)

9-19: Consider concurrency protection.
Rewriting and re-encoding the entire list in addStatus/removeStatus may cause data loss if multiple threads call these methods simultaneously. A dedicated queue or locking mechanism can help ensure atomic writes.


25-36: Cautious approach to decoding errors.
Returning an empty list on decode failure is safe but silently discards data. Consider logging additional context or providing fallback recovery steps if decoding is critical.


62-69: Ensure large data sets do not degrade performance.
Serializing and storing many statuses in user defaults may become expensive. If the list grows, consider an alternative storage strategy or routine cleanup to maintain efficiency.

WooCommerce/Classes/ViewRelated/Products/Media/ProductImagesSaver.swift (2)

132-136: Pattern matching needs to utilize the siteID and productID parameters

The case pattern discards the siteID and productID parameters with underscore placeholders, but doesn't use them in the function. Consider either using named variables to improve readability or documenting why these values are intentionally ignored.

-case .uploading(let uploadingAsset, _, _):
+case .uploading(let uploadingAsset, let _, let _):

Or better yet, if you need to use these values later:

-case .uploading(let uploadingAsset, _, _):
+case .uploading(let uploadingAsset, let siteID, let productID):

132-137: Consider handling extracted parameters differently.

The switch pattern extracts unused parameters with underscores, which is standard Swift practice but adds complexity.

A previous reviewer suggested omitting unused params. You could refactor this pattern to avoid needing to handle these unused parameters, perhaps through helper methods.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a6877f9 and a66c800.

📒 Files selected for processing (22)
  • Networking/Networking.xcodeproj/project.pbxproj (6 hunks)
  • Networking/Networking/ProductImageInBackground/ProductImageStatus.swift (1 hunks)
  • Networking/Networking/ProductImageInBackground/ProductImagesUserDefaultsStatuses.swift (1 hunks)
  • Networking/Networking/ProductImageInBackground/ProductOrVariationID.swift (1 hunks)
  • RELEASE-NOTES.txt (1 hunks)
  • WooCommerce/Classes/ServiceLocator/ProductImageUploader.swift (1 hunks)
  • WooCommerce/Classes/ViewRelated/Products/Cells/Product Images/ProductImagesCollectionViewDataSource.swift (1 hunks)
  • WooCommerce/Classes/ViewRelated/Products/Cells/Product Images/ProductImagesHeaderTableViewCell.swift (1 hunks)
  • WooCommerce/Classes/ViewRelated/Products/Media/Product+Media.swift (1 hunks)
  • WooCommerce/Classes/ViewRelated/Products/Media/ProductImageActionHandler.swift (5 hunks)
  • WooCommerce/Classes/ViewRelated/Products/Media/ProductImageStatus+Extension.swift (3 hunks)
  • WooCommerce/Classes/ViewRelated/Products/Media/ProductImagesCollectionViewController.swift (2 hunks)
  • WooCommerce/Classes/ViewRelated/Products/Media/ProductImagesSaver.swift (2 hunks)
  • WooCommerce/Classes/ViewRelated/Products/Media/ProductImagesViewController.swift (1 hunks)
  • WooCommerce/WooCommerce.xcodeproj/project.pbxproj (8 hunks)
  • WooCommerce/WooCommerceTests/Mocks/MockProductImageActionHandler.swift (1 hunks)
  • WooCommerce/WooCommerceTests/ViewRelated/Products/Edit Product/ProductVariationFormViewModel+ImageUploaderTests.swift (2 hunks)
  • WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageActionHandlerTests.swift (13 hunks)
  • WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageStatus+HelpersTests.swift (5 hunks)
  • WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageUploaderTests.swift (31 hunks)
  • WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImagesSaverTests.swift (6 hunks)
  • Yosemite/Yosemite/Model/Model.swift (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • RELEASE-NOTES.txt
👮 Files not reviewed due to content moderation or server errors (8)
  • WooCommerce/Classes/ViewRelated/Products/Cells/Product Images/ProductImagesHeaderTableViewCell.swift
  • WooCommerce/Classes/ViewRelated/Products/Media/Product+Media.swift
  • WooCommerce/WooCommerceTests/ViewRelated/Products/Edit Product/ProductVariationFormViewModel+ImageUploaderTests.swift
  • Networking/Networking/ProductImageInBackground/ProductOrVariationID.swift
  • WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageActionHandlerTests.swift
  • WooCommerce/Classes/ViewRelated/Products/Media/ProductImagesSaver.swift
  • WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageStatus+HelpersTests.swift
  • WooCommerce/Classes/ViewRelated/Products/Media/ProductImagesCollectionViewController.swift
🔇 Additional comments (74)
WooCommerce/Classes/ViewRelated/Products/Media/ProductImagesViewController.swift (1)

29-29: Update pattern matching to accommodate additional parameters

The case pattern for .remote has been updated to include two additional parameters (marked with underscores), aligning with the enhanced ProductImageStatus enum that now includes siteID and productID.

Yosemite/Yosemite/Model/Model.swift (1)

247-249: Added new type aliases for product image functionality

These new type aliases align with the PR objective of relocating image-related types to the Networking layer for better access during background uploads, which is essential for supporting the storage of product image upload statuses in UserDefaults.

WooCommerce/WooCommerceTests/Mocks/MockProductImageActionHandler.swift (1)

6-8: Added imports for relocated enums

Added imports for the ProductImageStatus, ProductImageAssetType, and ProductOrVariationID enums that have been moved to the Networking layer. This ensures the mock handler can work with the same types as the actual implementation.

WooCommerce/WooCommerce.xcodeproj/project.pbxproj (1)

605-605: Project structure organization aligns with PR objectives

The renaming of ProductImageStatus.swift to ProductImageStatus+Extension.swift and addition of MockProductImageActionHandler.swift properly support the architectural changes needed for background image uploads. These references are consistently updated throughout the project file.

Also applies to: 1412-1412, 3821-3821, 4572-4572, 7455-7455, 9960-9960, 16253-16253, 17790-17790

WooCommerce/Classes/ViewRelated/Products/Cells/Product Images/ProductImagesHeaderTableViewCell.swift (5)

75-75:

❓ Verification inconclusive

Update case pattern matching to handle new parameters correctly

The case matching has been updated to support additional parameters in ProductImageStatus.remote, but the new parameters are ignored with _ placeholders instead of being explicitly named. Consider whether these parameters provide useful context that should be passed to onImageSelected or other handlers.


🏁 Script executed:

#!/bin/bash
# Check how onImageSelected is used in the codebase
rg "onImageSelected\??\(" -A 3

Length of output: 2826


Clarify Handling of Additional Parameters in the .remote Case

In ProductImagesHeaderTableViewCell.swift (around line 75), the .remote case is currently written as:

case .remote(let image, _, _):

The new parameters introduced in ProductImageStatus.remote are being ignored with _ placeholders. Since the onImageSelected callback in this file is invoked with image and indexPath (as seen in the surrounding code), please consider the following:

  • Evaluate Parameter Relevance: Determine whether the additional parameters provide useful context (e.g., metadata or state information) that should be forwarded to onImageSelected or used in another way.
  • Improve Code Clarity: If these parameters are meant to be used later, name them descriptively instead of using _. Conversely, if they are intentionally ignored, please add an inline comment to document the decision.

75-75: Code correctly updated for additional parameters.

The pattern matching for .remote case has been updated to handle the new siteID and productID parameters, correctly extracting only the image while ignoring the additional values with underscores.


79-79: Code correctly updated for additional parameters.

The pattern matching for .uploadFailure case has been updated to handle the new siteID and productID parameters, correctly extracting only the necessary asset and error while ignoring the additional values.


75-75: Update case pattern to include new parameters

The case pattern for .remote has been updated to include additional parameters for siteID and productID. These parameters are currently not used in this context but are correctly ignored with wildcards.


79-79: Update case pattern to include new parameters

The case pattern for .uploadFailure has been updated to include additional parameters for siteID and productID. These parameters are correctly ignored using wildcards since they're not needed in this context.

WooCommerce/Classes/ViewRelated/Products/Media/Product+Media.swift (5)

6-6: Updated image status construction to include site and product IDs

The changes correctly update the ProductImageStatus.remote initialization to include siteID and productID, which is essential for the background upload functionality. The implementation consistently applies this pattern to both ProductFormDataModel and Product extensions.

Also applies to: 12-12


6-6: Implementation correctly updated with required parameters.

The imageStatuses computed property has been updated to include the new required parameters (siteID and productID) when creating each ProductImageStatus.remote instance. This aligns with the enhanced structure for supporting background image uploads.


12-12: Implementation correctly updated with required parameters.

The imageStatuses computed property in the Product extension has been properly updated to include the required siteID and productID parameters, consistent with the changes in the ProductFormDataModel extension.


6-6: LGTM: Add site and product identification to remote image statuses

The imageStatuses property now correctly includes the siteID and productID parameters when creating remote image statuses, which is essential for supporting background image uploads as mentioned in the PR objectives.


12-12: LGTM: Add site and product identification to remote image statuses

Similar to the ProductFormDataModel extension, this implementation correctly includes the siteID and productID parameters when creating remote image statuses.

WooCommerce/WooCommerceTests/ViewRelated/Products/Edit Product/ProductVariationFormViewModel+ImageUploaderTests.swift (6)

11-12: Constants added for new required test parameters

Adding the siteID and productID constants properly sets up the test environment for the updated ProductImageStatus implementation. Using static values here is appropriate for unit testing.


28-30: Updated mock handler initialization with required parameters

The test has been correctly updated to pass the new required parameters to the MockProductImageActionHandler initialization, ensuring that the test reflects the updated function signatures.


11-12: Test properties added correctly.

Added properties for siteID and productID with appropriate test values. The productID correctly uses the new ProductOrVariationID enum to create a product identifier.


28-30: Test correctly updated with new parameters.

The MockProductImageActionHandler initialization has been updated to include the new required parameters (siteID and productID) for the .uploading status, ensuring the tests properly represent the updated implementation.


11-12: LGTM: Add test properties for site and product identification

These properties provide the necessary context for testing with the updated ProductImageStatus enum that now requires siteID and productID parameters.


28-30: LGTM: Update test case with new parameters

The mock ProductImageActionHandler initialization has been properly updated to include the siteID and productID parameters in the .uploading status.

Networking/Networking/ProductImageInBackground/ProductOrVariationID.swift (3)

1-54: Add tests for Codable conformance

The ProductOrVariationID enum is well-designed with proper Codable implementation for both cases. However, as mentioned in previous feedback, please add unit tests to confirm both encoding and decoding work as expected, especially given the complexity of handling different enum cases.

#!/bin/bash
# Check if there are any unit tests for this new file
rg -l "ProductOrVariationID" --type swift | grep -i test

1-54: Well-structured enum for representing product and variation IDs.

The implementation looks solid with:

  • Clear cases for both product and variation IDs
  • A computed id property that handles both cases appropriately
  • Proper Codable implementation with custom encoding and decoding

As mentioned in a previous review, since this enum conforms to Codable, please ensure you add tests to verify that both encoding and decoding work correctly for all cases.

What are best practices for testing Swift Codable implementation?

1-54: Add unit tests for Codable implementation

The implementation of this new enum looks good. It provides a structured way to handle product and variation identifiers and properly implements the Codable protocol for persistence in UserDefaults.

However, as noted in a previous review, you should add unit tests to verify that both encoding and decoding work as expected for this enum.

WooCommerce/Classes/ViewRelated/Products/Media/ProductImageStatus+Extension.swift (3)

8-8: Nit: Unused parameters in .remote.

It looks like the second and third parameters are not used here. If these parameters aren’t necessary yet, consider removing them to avoid confusion. If they’re intended for future usage, add a comment explaining their purpose.


51-51: Nit: Unused parameters in .uploading.

Similar to the .remote case, the second and third parameters are currently unused. Removing them or documenting their expected usage helps keep the code clean and maintainable.


60-60: Consider leveraging unused parameters for the drag identifier.

If the additional parameters in .remote refer to siteID or productID, incorporating them into the drag item identifier could improve uniqueness across multiple sites or products. If they’re truly unneeded here, remove them or add a comment clarifying their future intent.

Networking/Networking.xcodeproj/project.pbxproj (3)

2701-2709: Well-organized project structure for the new feature.

Creating a dedicated group for the image background processing files (ProductImageStatus.swift, ProductOrVariationID.swift, and ProductImagesUserDefaultsStatuses.swift) is a good organizational practice. This makes the project structure more maintainable by keeping related files together, especially as this feature will be expanded in future PRs.


2944-2944: Appropriate placement of the new group in the project hierarchy.

The placement of the ProductImageInBackground group at the root level of the project is suitable for its functionality, making it easily accessible for future development while keeping it properly encapsulated.


482-484: Complete and consistent project file references.

All required project file references for the new Swift files have been properly added to the necessary sections of the project file (PBXBuildFile, PBXFileReference, and in build phases). This ensures the files will be correctly included in the build process.

Also applies to: 1696-1698, 5333-5333, 5378-5378, 5385-5385

Networking/Networking/ProductImageInBackground/ProductImageStatus.swift (3)

8-8: Add unit tests for Codable conformance.

Previous review feedback (lines 8-8) requested tests for encoding/decoding the ProductImageStatus enum. These tests are still missing.


95-99: Revisit equality comparison for errors.

Using (lError as NSError) == (rError as NSError) may yield false negatives when bridging Error to NSError if underlying data changes. Consider storing and comparing only a domain, code, and standardized userInfo keys to avoid unexpected mismatches.


107-107: Add unit tests for Codable conformance.

Similar to ProductImageStatus, ProductImageAssetType also now conforms to Codable. Ensure that round-trip encoding/decoding tests validate all associated values.

WooCommerce/Classes/ServiceLocator/ProductImageUploader.swift (1)

7-9: Imports look good.

The newly added imports for ProductImageStatus, ProductImageAssetType, and ProductOrVariationID correctly reflect the refactor to manage these types in the Yosemite module.

WooCommerce/Classes/ViewRelated/Products/Cells/Product Images/ProductImagesCollectionViewDataSource.swift (1)

54-56: Omit unused parameters or handle them if necessary.

The switch uses _ for siteID and productID. If they are not needed, consider removing these parameters from the enum or removing the placeholders to avoid confusion.

Also applies to: 63-63

WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageActionHandlerTests.swift (5)

11-12: Appropriate introduction of new properties for test setup.

The addition of siteID and productID properties provides the necessary test context for the updated ProductImageStatus model which now requires these identifiers.


14-14: Good test naming update for consistency.

The test method naming has been updated from camelCase to snake_case, which aligns better with Swift unit testing conventions and improves readability.


401-441: Good test coverage for product ID propagation.

This new test is crucial as it verifies that the updateProductID method correctly propagates changes to existing upload statuses. This ensures that when a product transitions from local to remote state (getting a server-assigned ID), all associated uploads are updated accordingly.


401-441: Good implementation of test for product ID updates!

The new test verifies that updating a product ID properly propagates to existing upload statuses, which is crucial for maintaining data consistency when switching from a local to remote product ID.


401-441: Good implementation of product ID update test.

This test correctly verifies that the product ID is properly updated in existing upload statuses when updateProductID is called.

Consider adding a brief comment explaining the significance of updating from local to remote product ID to make the test purpose clearer.

WooCommerce/Classes/ViewRelated/Products/Media/ProductImagesSaver.swift (4)

132-133: Correctly updated pattern matching for new parameters.

The pattern matching has been updated to accommodate the additional parameters in the uploading case, allowing the code to continue working with the new enum structure.


151-151: Required parameters added to remote image status.

The implementation now correctly sets the siteID and productID when creating a remote image status, which is essential for tracking these images in UserDefaults for background uploads.


150-152: Good implementation of site and product tracking in ProductImageStatus update

The status now properly includes siteID and productID parameters when updating to remote state, which is necessary for consistent background uploads.


150-152: Added new parameters correctly.

The updateProductImageStatus method has been updated to include the necessary siteID and productID parameters in the remote status.

WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageStatus+HelpersTests.swift (5)

9-10: Well-defined test properties for consistent tests.

Good practice to define these properties once at the class level rather than repeating the values in each test case, enhancing maintainability.


16-16: Comprehensive test updates for the modified enum cases.

All test cases have been consistently updated to include the required siteID and productID parameters across various scenarios, ensuring the helper methods continue to work correctly with the enhanced data model.

Also applies to: 25-26, 41-42, 50-51, 61-61, 74-74


9-10: Good addition of test properties for consistent testing

The addition of siteID and productID properties enables consistent testing across all test methods.


9-10: Good test setup with new parameters.

The addition of the siteID and productID properties as class properties makes the tests cleaner and easier to update if values need to change.


41-42: Properly updated test cases.

The test cases correctly use the new parameters required by the ProductImageStatus enum.

WooCommerce/Classes/ViewRelated/Products/Media/ProductImagesCollectionViewController.swift (7)

99-100: Properly updated pattern matching for upload failures.

The upload failure case pattern has been updated to include all new parameters while preserving existing functionality.


189-190: Correctly modified failure case handling.

The didSelectItemAt method has been updated to work with the new parameter structure while maintaining the same behavior.


90-91: Unused parameters in remote case pattern

The case pattern ignores siteID and productID with underscores, but doesn't document why. Consider adding a comment explaining why these values are intentionally ignored or use named variables for clarity.


92-98: Unused parameters in uploading case pattern

Similar to the remote case, the siteID and productID parameters are ignored without explanation.


99-106: Unused parameters in uploadFailure case pattern

The case pattern has four parameters but two are ignored with underscores. Consider adding a comment explaining why or use named variables for clarity.


189-191: Consistent pattern matching in didSelectItemAt

The error handling logic for uploadFailure correctly extracts just the relevant parameters (asset and error) and ignores siteID and productID that aren't needed in this context.


189-190: Correctly updated case pattern with error parameter.

This switch case correctly extracts the necessary parameters including the error.

WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImagesSaverTests.swift (6)

47-47: Well-structured test assertion.
No functional issue stands out. Including siteID and productID in the uploading status enhances clarity of the test scenario.


88-88: Good inclusion of identifiers in the status check.
Maintaining consistency in these tests helps verify logic across different scenarios.


144-144: Consistent usage of uploading status.
This matches the approach used in similar test cases for ensuring correctness after a product save attempt fails.


159-161: Verify correctness of product ID in a variation test.
This test is named for variation handling, but the uploading status references .product(id: productID) instead of .variation(...). Verify that this mismatch is intentional:

- let actionHandler = MockProductImageActionHandler(productImageStatuses: [.uploading(asset: asset, siteID: siteID, productID: .product(id: productID))])
+ let actionHandler = MockProductImageActionHandler(productImageStatuses: [.uploading(asset: asset, siteID: siteID, productID: .variation(productID: productID, variationID: 134))])

252-254: Accurate use of variation product ID.
This status properly reflects .variation(...), ensuring the test exercises variation-specific logic.


282-282: Product scenario coverage maintained.
Including .product(id: productID) consistently verifies site-specific logic for standard products.

Networking/Networking/ProductImageInBackground/ProductImagesUserDefaultsStatuses.swift (1)

38-56: Clarify handling of productID == nil.
The filter case implies a scenario where productID can be absent, but from usage elsewhere, productID often appears mandatory. Confirm that ignoring statuses is intended if productID is nil.

WooCommerce/Classes/ViewRelated/Products/Media/ProductImageActionHandler.swift (7)

165-167: Clear representation of remote statuses.
Storing the siteID and productOrVariationID ensures that future retrieval or updates remain unambiguous.


178-186: Variation vs. standard product logic is well-delineated.
Replacing the entire status array for variations vs. appending for products is well-structured. Ensure calling code accounts for this difference in user flows.


259-268: Correct mapping of upload statuses on product ID update.
This approach systematically replaces the local ID with the remote one. Ensure that downstream references are updated to avoid stale IDs.


279-283: Strict matching for remote images.
Filtering by both siteID and productOrVariationID helps prevent accidental removal of unrelated images.


317-321: Properly scoped index search.
Matching asset, siteID, and productOrVariationID in index(of:) ensures precise status identification. Reduces risk of collisions if multiple uploads share the same asset reference.


329-329: Effective success-state update.
Moving from .uploading to .remote is clearly handled, and notifying observers is well-placed here.

Also applies to: 334-334


338-338: Thoughtful error handling.
Transitioning from .uploading to .uploadFailure notifies all listeners and retains enough context to troubleshoot failures.

Also applies to: 342-342

WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageUploaderTests.swift (5)

27-27: Consider using productID directly as suggested in previous review.

There's an inconsistency in how the product ID is referenced. In some places, productID is used directly, while in others, .product(id: productID.id) is used, which seems redundant since productID already contains this information.

-                                                                          productOrVariationID: .product(id: productID.id),
+                                                                          productOrVariationID: productID,

Also applies to: 40-40, 44-44, 49-49


85-87: LGTM! Improved code formatting.

The if-statement block now follows proper Swift style guidelines with braces on separate lines.


72-72: Verify fake product creation with ID mapping.

When creating fake test products, the productID.id is now used. Make sure this properly maps to what the model expects and that all product-related functionality works correctly with this change.

Also applies to: 632-632


16-19:

✅ Verification successful

Verify the implementation of MockProductImagesProductIDUpdater.

The tests now use a new dependency MockProductImagesProductIDUpdater. Ensure that this mock properly implements all required functionality and that it faithfully reproduces the behavior of the real implementation for testing.

Also applies to: 190-193, 225-228


🏁 Script executed:

#!/bin/bash
# Check the implementation of MockProductImagesProductIDUpdater

# Search for the mock class definition
rg -A 10 "class MockProductImagesProductIDUpdater" --type swift

Length of output: 1205


Verification Complete: Mock Implementation Meets Testing Requirements

The implementation of MockProductImagesProductIDUpdater (located in WooCommerce/WooCommerceTests/Mocks/MockProductImagesProductIDUpdater.swift) correctly conforms to ProductImagesProductIDUpdaterProtocol. The mock sets the updateImageProductIDWasCalled flag and returns a fake Media as expected, which appears sufficient for the testing scenarios in use (including the instances at lines 16-19, 190-193, and 225-228). No additional changes are required.


195-197:

✅ Verification successful

Ensure ProductImageStatus properly includes site and product ID parameters.

The ProductImageStatus enum has been updated to include siteID and productID parameters. Verify that these parameters are properly handled in the actual implementation, especially for background uploads.

Also applies to: 232-234


🏁 Script executed:

#!/bin/bash
# Check the implementation of ProductImageStatus

# Search for the enum definition and its cases
rg -A 20 "enum ProductImageStatus" --type swift

Length of output: 2253


Background Uploads: Confirm Parameter Handling

The enum definition of ProductImageStatus (located in Networking/Networking/ProductImageInBackground/ProductImageStatus.swift) shows that all cases, including .remote and .uploading, now correctly require and handle both siteID and productID parameters. The test instantiations in WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageUploaderTests.swift for lines 195–197 and 232–234 match this updated implementation.

No further action is required here, but please verify that all background upload flows correctly propagate and utilize these parameters where necessary.

…ad of a hardcoded value.

- Change `ProductImageUploaderTests.swift` to correctly assign `productID.id` to `remoteProductID`.
Copy link
Contributor

@itsmeichigo itsmeichigo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Paolo, I tested the image upload and reordering on this PR and it seems to still work as before. However, it's really hard for me to approve this PR given that the file ProductImagesUserDefaultsStatuses is not ready.

I understand that you are changing the file in a subsequent PR - how about removing it from this branch and adding it only to your subsequent PR? That way, we would not have to merge unfinished code.

@pmusolino
Copy link
Member Author

Thank you @itsmeichigo for your patience and review. I will split the next PRs into multiple PRs (I have at least 3 in the queue), because there is so much code, and I removed from this one what has been heavily modified in a future PR (in this case ProductImagesUserDefaultsStatuses). 6ea99b3

Copy link
Contributor

@itsmeichigo itsmeichigo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Paolo for the updates! I see that you're adding extra tests in a separate PR, so I'm approving this PR for now. Really appreciate your patience!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature: add/edit products Related to adding or editing products. feature: product details Related to adding or editing products, including Product Settings.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants